from __future__ import annotations
from dataclasses import dataclass
from functools import reduce
import pathlib
from posixpath import normpath
import re
from sys import stderr
from typing import Callable
from clang import cindex


def lv_get_decay(type: cindex.Type) -> str | None:
    spell = type.get_canonical().get_declaration().type.get_canonical().spelling
    match = re.compile(r'^(?:struct\s+)?(_?lv_\w+)$').match(spell)
    return match[1] if match else match


@dataclass
class LvMethodItem:
    define: cindex.Cursor
    type: cindex.Type | None
    struct: str | None


unprocess: dict[str, cindex.Cursor] = {}
lvmethods: dict[str, LvMethodItem] = {}
lvObjectMethods: dict[str, LvMethodItem] = {}
lvWdigets: dict[str, dict[str, LvMethodItem]] = {}
lvStructs: dict[str, dict[str, LvMethodItem]] = {}
lvWdigetsParent: dict[str, str] = {'keyboard': 'btnmatrix'}
lvTypes: dict[str, cindex.Type] = {}


def scan_func(item: LvMethodItem):
    path: str = normpath(item.define.extent.start.file.name)
    if match := re.compile(r'.+(?:/widgets/lv_(\w+).h|/widgets/(\w+)/lv_\2.h)$').match(path):
        name = match[1] or match[2]
        if item.define.spelling.startswith(f'lv_{name}_'):
            if name not in lvWdigets:
                lvWdigets[name] = {}
            if item.define.spelling != f'lv_{name}_create':
                lvWdigets[name][item.define.spelling] = item
                return
    if item.define.spelling.startswith('lv_obj_'):
        if item.define.spelling != 'lv_obj_create':
            lvObjectMethods[item.define.spelling] = item
        return
    match = re.compile(r'.+/lv_(\w+).h').fullmatch(path)
    if (name := match and match[1]) and item.define.spelling.startswith(f'lv_{name}_') and (not item.struct or re.compile(r'_?lv_{name}_t').fullmatch(item.struct)):
        if name not in lvStructs:
            lvStructs[name] = {}
        if item.define.spelling == f'lv_{name}_create':
            item.type = None
            item.struct = None
        lvStructs[name][item.define.spelling] = item
        return
    if item.struct:
        match = re.compile(r'_?lv_(\w+)_t').fullmatch(item.struct)
        if (name := match and match[1]) and item.define.spelling.startswith(f'lv_{name}_'):
            if name not in lvStructs:
                lvStructs[name] = {}
            if item.define.spelling == f'lv_{name}_create':
                item.type = None
                item.struct = None
            lvStructs[name][item.define.spelling] = item
            return
        else:
            lvmethods[item.define.spelling] = item
    else:
        unprocess[item.define.spelling] = item.define


def convert_name(prefix: str, name: str):
    components = name.removeprefix(prefix).split('_')
    return reduce(lambda x, y: x + y.title(), components[1:], components[0])


def gen_type_name(raw: cindex.Type) -> str:
    name = raw.spelling
    name = 'bool' if name == '_Bool' else name
    return name


def gen_parm_def(parm: cindex.Cursor) -> str:
    if parm.type.kind == cindex.TypeKind.INCOMPLETEARRAY:
        type = gen_type_name(parm.type.element_type)
        sep = '' if type[-1] in ['&', '*'] else ' '
        return type+sep+parm.spelling+'[]'
    else:
        type = gen_type_name(parm.type)
        sep = '' if type[-1] in ['&', '*'] else ' '
        return type+sep+parm.spelling


def gen_method(item: LvMethodItem, prefix: str, result_type: str = None) -> str:
    result_type = result_type or gen_type_name(item.define.result_type)
    if item.struct:
        is_void = gen_type_name(item.define.result_type) == result_type and result_type == 'void'
        parms = iter(item.define.get_arguments())
        first = next(parms)
        is_const = first.type.get_pointee().is_const_qualified()
        result_type = ('const TSelf &' if is_const else 'TSelf &') if is_void else result_type
        sep = '' if result_type[-1] in ['&', '*'] else ' '
        out = 'inline ' + result_type + sep + convert_name(prefix, item.define.spelling) + '('
        out += ', '.join(map(gen_parm_def, parms))+')'
        if is_const:
            out += ' const'
        parms = iter(item.define.get_arguments())
        next(parms)
        result_type = f'({result_type})' if not is_void and result_type != gen_type_name(item.define.result_type) else ''
        out += ' { return '+result_type + item.define.spelling + '(raw()'
        parms = ', '.join([x.spelling for x in parms])
        if len(parms) != 0:
            out += ', '+parms
        out += '), _self(); }' if is_void else '); }'
        return out
    else:
        sep = '' if result_type[-1] in ['&', '*'] else ' '
        out = 'static inline ' + result_type + sep + convert_name(prefix, item.define.spelling) +\
            '(' + ', '.join(map(gen_parm_def, item.define.get_arguments()))+')'
        result_type = f'({result_type})' if result_type != gen_type_name(item.define.result_type) else ''
        out += ' { return '+result_type + item.define.spelling + \
            '(' + ', '.join([x.spelling for x in item.define.get_arguments()])+'); }'
        return out


def gen_result_type(item: LvMethodItem) -> str:
    type = item.define.result_type
    if type.kind == cindex.TypeKind.POINTER:
        raw = type.get_pointee()
        if lv_get_decay(raw) == '_lv_obj_t':
            return 'LvRawPtr<const lv_obj_t>' if raw.is_const_qualified() else 'LvObjPtr'
    return gen_type_name(item.define.result_type)


def gen_file(path: str, prefix: str, methods: dict[str, LvMethodItem], template: str | None = None, result_type: Callable[[LvMethodItem], str] = gen_result_type):
    if pathlib.Path(path).exists():
        with open(path) as f:
            template = f.read()
    status = 0
    mark = re.compile(r'^(\s*)//\+gen')
    with open(path, 'w') as file:
        for line in template.splitlines():
            match status:
                case 0:
                    print(line, file=file)
                    match = mark.match(line)
                    if not match:
                        continue
                    status = 1
                    indent = match[1]
                    try:
                        for item in methods.values():
                            print(indent, gen_method(item, prefix, result_type=result_type(item)), sep='', file=file)
                    except Exception as e:
                        print(e, file=stderr)
                case 1:
                    if mark.match(line):
                        status = 2
                        print(line, file=file)
                case 2:
                    print(line, file=file)


def gen_obj_getter(name: str) -> str | None:
    parent = lvWdigetsParent.get(name, None)
    if parent == None:
        return 'obj'
    first = next(lvTypes[f'lv_{name}_t'].get_fields())
    # assert first.type == lvTypes[f'lv_{parent}_t']
    return f'{first.spelling}.{gen_obj_getter(parent)}'


def main():
    outdir = pathlib.PurePath(__file__).parent
    tu = cindex.TranslationUnit.from_source(
        'Libs/lv_binding_cpp/lvgl/lvgl.h',
        args=['--target=x86_64-pc-windows-gnu', '-DLV_CONF_INCLUDE_SIMPLE', '-DLV_LVGL_H_INCLUDE_SIMPLE', f'-I{outdir}',
              f'-I{outdir}/../../App', '-isystemD:/DXDK/msys64/ucrt64/lib/clang/14.0.4/include', '-isystemD:/DXDK/msys64/ucrt64/include/c++/v1', '-isystemD:/DXDK/msys64/ucrt64/include', f'-isystem{outdir}/..', f'-isystem{outdir}/lvgl'],
        options=cindex.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD | cindex.TranslationUnit.PARSE_INCOMPLETE | cindex.TranslationUnit.PARSE_SKIP_FUNCTION_BODIES
    )
    if len(tu.diagnostics) != 0:
        for dia in tu.diagnostics:
            print(dia)
        exit()
    for item in tu.cursor.get_children():
        match item.kind:
            case cindex.CursorKind.STRUCT_DECL:
                if not (item.spelling.startswith('lv_') or item.spelling.startswith('_lv_')):
                    continue
                lvTypes[item.spelling] = item.type
            case cindex.CursorKind.TYPEDEF_DECL:
                if not (item.spelling.startswith('lv_') or item.spelling.startswith('_lv_')):
                    continue
                if (type := item.canonical.underlying_typedef_type.get_canonical()).kind == cindex.TypeKind.RECORD:
                    lvTypes[item.spelling] = type
            case cindex.CursorKind.FUNCTION_DECL:
                if not item.spelling.startswith('lv_'):
                    continue
                reciver = next(item.get_arguments(), None)
                if reciver and reciver.type.kind == cindex.TypeKind.POINTER:
                    type = reciver.type.get_pointee()
                    name = lv_get_decay(type)
                    scan_func(LvMethodItem(define=item, type=type, struct=name))
                else:
                    scan_func(LvMethodItem(define=item, type=None, struct=None))

    def obj_result_type(item: LvMethodItem) -> str:
        match item.define.spelling:
            case 'lv_obj_get_group': return 'LvGroup*'
            case _: return gen_result_type(item)
    gen_file(outdir.joinpath('lv_cpp', 'core', 'LvObj.h'), prefix='lv_obj_',
             methods=lvObjectMethods, result_type=obj_result_type)
    for [name, methods] in lvWdigets.items():
        tname = name.title()
        parent = lvWdigetsParent.get(name, 'obj')
        getter = ''
        if parent != 'obj':
            getter = f'\n\t\tstatic inline lv_obj_t &get(lv_{name}_t &{name}) noexcept {{ return {name}.{gen_obj_getter(name)}; }};'
        parent = parent.title()
        record = '' if f'lv_{name}_t' in lvTypes else f'\n\tstruct lv_{name}_t{{lv_obj_t obj;}};\n'
        path = outdir.joinpath('lv_cpp', 'widgets', f'Lv{tname}.h')
        pathlib.Path(path.parent).mkdir(parents=True, exist_ok=True)
        gen_file(path, prefix=f'lv_{name}_', methods=methods, template=f"""
#ifndef LV{name.upper()}_H_
#define LV{name.upper()}_H_

#include ".{'./core' if parent == 'Obj' else ''}/Lv{parent}.h"

namespace lvglpp
{{
	template <typename PROTYPE>
	class LvT{tname} : public LvT{parent}<PROTYPE>
	{{
	protected:
		using typename LvT{parent}<PROTYPE>::TSelf;
		using LvT{parent}<PROTYPE>::_self;

	public:
		using LvT{parent}<PROTYPE>::raw;
		using LvT{parent}<PROTYPE>::LvT{parent};
		//+gen

		//+gen
	}};
{record}
	template <>
	struct LvProtypeTraits<lv_{name}_t>
	{{
		using TWrapped = LvT{tname}<lv_{name}_t>;
		static constexpr auto &obj_class = lv_{name}_class;
		static constexpr auto create = lv_{name}_create;{getter}
	}};

	using Lv{tname} = LvT{tname}<lv_{name}_t &&>;
	using Lv{tname}Ptr = LvRawPtr<lv_{name}_t>;

}} /* namespace lvglpp */

#endif /* LV{name.upper()}_H_ */
""")
    gen_file(outdir.joinpath('lv_cpp', 'misc', 'LvStyle.h'), prefix='lv_style_',
             methods=lvStructs.pop('style'), result_type=obj_result_type)
    outfile = open(f'{outdir}/.unprocess.txt', 'w')
    print('\n\n\nstructs:', file=outfile)
    for [name, methods] in lvStructs.items():
        print(f'\n\nstructs {name}:', file=outfile)
        for item in methods.values():
            print(gen_method(item, prefix=f'lv_{name}_'), file=outfile)
    print('\n\n\nstruct:', file=outfile)
    for name in lvmethods:
        print(lvmethods[name].define.displayname, normpath(
            lvmethods[name].define.extent.start.file.name), file=outfile)
    print('\n\n\nunprocess:', file=outfile)
    for name in unprocess:
        print(unprocess[name].displayname, normpath(
            unprocess[name].extent.start.file.name), file=outfile)


if __name__ == '__main__':
    main()
